#include "Shader.hpp"

#include "Exception.hpp"
#include "gl.hpp"
#include <glm/gtc/type_ptr.hpp>
#include <fstream>

using namespace glm;
using namespace std;



std::string GetFileContent(const string& filename)
{
	std::ifstream in(filename.c_str(), std::ios::in | std::ios::binary);
	if(in)
	{
		std::string contents;
		in.seekg(0, std::ios::end);
		contents.resize(in.tellg());
		in.seekg(0, std::ios::beg);
		in.read(&contents[0], contents.size());
		in.close();
		return(contents);
	}
	THROW(Exception("file could not be read") << FilenameInfo(filename));
}


namespace Detail
{
	void ShaderProgram::Set() const
	{
		glUseProgram(m_ProgramObject);
	}

	unsigned int ShaderProgram::GetUniformLocation(string name) const
	{
		GLint loc=glGetUniformLocation(m_ProgramObject, name.c_str());
		if(-1==loc)
		{
			//THROW(Exception("Uniform variable not found: " + name));
		}
		return loc;
	}

	void ShaderProgram::SetUniform(unsigned int location, float value)
	{
		glUniform1f(location, value);
	}

	void ShaderProgram::SetUniform(unsigned int location, const glm::vec2& value)
	{
		glUniform2f(location, value.x, value.y);
	}

	void ShaderProgram::SetUniform(unsigned int location, const glm::vec3& value)
	{
		glUniform3f(location, value.x, value.y, value.z);
	}
	void ShaderProgram::SetUniform(unsigned int location, const glm::vec4& value)
	{
		glUniform4f(location, value.x, value.y, value.z, value.w);
	}
	void ShaderProgram::SetUniform(unsigned int location, const glm::mat3& value)
	{
		glUniformMatrix3fv(location, 1, false, value_ptr(value));
	}
	void ShaderProgram::SetUniform(unsigned int location, const glm::mat4& value)
	{
		glUniformMatrix4fv(location, 1, false, value_ptr(value));
	}
	void ShaderProgram::SetUniform(unsigned int location, int value)
	{
		glUniform1i(location, value);
	}
	void ShaderProgram::SetUniform(unsigned int location, unsigned int value)
	{
		glUniform1ui(location, value);
	}





	ShaderPart::ShaderPart(unsigned int type) :
		m_ShaderObject(0),
		m_Type(type)
	{
	}

	void ShaderPart::Load(std::string filename)
	{
		try
		{
			string Source=GetFileContent(filename);
			Create(Source);
		}
		catch(Exception &e)
		{
			e << ErrorInfo("Shader File couldn't be loaded!");
			throw;
		}
	}

	void ShaderPart::Create(std::string source)
	{
		m_ShaderObject = glCreateShader(m_Type);
		if(0 == m_ShaderObject)
			THROW(Exception("shader object could not be created") << VertexSourceInfo(source));
		const auto Text=reinterpret_cast<const GLchar*>(source.c_str());
		glShaderSource(m_ShaderObject, 1, &Text, 0);
		glCompileShader(m_ShaderObject);

		int success;
		glGetShaderiv(m_ShaderObject, GL_COMPILE_STATUS, &success);

		if(!success)
		{
			char Text[512];
			int Length;
			glGetShaderInfoLog(m_ShaderObject, 510, &Length, Text);
			THROW(Exception("Shader Compiling error!") << DescriptionInfo(Text) << VertexSourceInfo(source));
		}
	}

	inline unsigned int ShaderPart::GetObject() const
	{
		return m_ShaderObject;
	}

}

void Shader::Load(string vertexFile, string fragmentFile, std::string geometryFile)
{
	m_VertexShader.Load(vertexFile);
	m_FragmentShader.Load(fragmentFile);
	if(!geometryFile.empty())
		m_GeometryShader.Load(geometryFile);

	ProcessShader();
}

void Shader::Create(string vertexSource, string fragmentSource, std::string geometrySource)
{
	m_VertexShader.Create(vertexSource);
	m_FragmentShader.Create(fragmentSource);
	if(!geometrySource.empty())
		m_GeometryShader.Create(geometrySource);

	ProcessShader();
}

void Shader::BindTexture(std::string Name, unsigned int Texture, unsigned int Channel) const
{
	SetUniform(Name, Channel);
	
	glActiveTexture(GL_TEXTURE0+Channel);
	glBindTexture(GL_TEXTURE_2D, Texture);
}

void Shader::BindTexture(unsigned int Texture, unsigned int Channel) const
{
	glActiveTexture(GL_TEXTURE0+Channel);
	glBindTexture(GL_TEXTURE_2D, Texture);
}


void Shader::ProcessShader()
{
	m_ProgramObject=glCreateProgram();
	glAttachShader(m_ProgramObject, m_VertexShader.GetObject());
	glAttachShader(m_ProgramObject, m_FragmentShader.GetObject());
	if(0 != m_GeometryShader.GetObject())
		glAttachShader(m_ProgramObject, m_GeometryShader.GetObject());

	glLinkProgram(m_ProgramObject);

	//check for errors:
	int Success=0;
	glGetProgramiv(m_ProgramObject, GL_LINK_STATUS, &Success);
	if(!Success)
	{
		char Text[512];
		int Length;
		glGetProgramInfoLog(m_ProgramObject, 510, &Length, Text);
		THROW(Exception("Shader couldn't be linked") << DescriptionInfo(Text));
	}

	glValidateProgram(m_ProgramObject);
	glGetProgramiv(m_ProgramObject, GL_VALIDATE_STATUS, &Success);
	if(!Success)
	{
		char Text[512];
		int Length;
		glGetProgramInfoLog(m_ProgramObject, 510, &Length, Text);
		THROW(Exception("Shader couldn't be validated") << DescriptionInfo(Text));
	}
}



VertexShader::VertexShader():
ShaderPart(GL_VERTEX_SHADER)
{
}

VertexShader& VertexShader::operator=(VertexShader s)
{
	m_ShaderObject = s.m_ShaderObject;
	return *this;
}


FragmentShader::FragmentShader():
ShaderPart(GL_FRAGMENT_SHADER)
{
}

FragmentShader& FragmentShader::operator=(FragmentShader s)
{
	m_ShaderObject = s.m_ShaderObject;
	return *this;
}


GeometryShader::GeometryShader():
ShaderPart(GL_GEOMETRY_SHADER)
{
}

GeometryShader& GeometryShader::operator=(GeometryShader s)
{
	m_ShaderObject = s.m_ShaderObject;
	return *this;
}


ComputeShader::ComputeShader():
ShaderPart(GL_COMPUTE_SHADER)
{
}

void ComputeShader::Create(std::string source)
{
	ShaderPart::Create(source);

	m_ProgramObject=glCreateProgram();
	glAttachShader(m_ProgramObject, m_ShaderObject);
	glLinkProgram(m_ProgramObject);

	//check for errors:
	int Success=0;
	glGetProgramiv(m_ProgramObject, GL_LINK_STATUS, &Success);
	if(!Success)
	{
		char Text[512];
		int Length;
		glGetProgramInfoLog(m_ProgramObject, 510, &Length, Text);
		THROW(Exception("Shader couldn't be linked") << DescriptionInfo(Text));
	}

	glValidateProgram(m_ProgramObject);
	glGetProgramiv(m_ProgramObject, GL_VALIDATE_STATUS, &Success);
	if(!Success)
	{
		char Text[512];
		int Length;
		glGetProgramInfoLog(m_ProgramObject, 510, &Length, Text);
		THROW(Exception("Shader couldn't be validated") << DescriptionInfo(Text));
	}
}

void ComputeShader::Run(glm::uvec3 workGroupSize)
{
	glDispatchCompute(workGroupSize.x, workGroupSize.y, workGroupSize.z);
}
